// Copyright (c) 2023 CHANGLEI. All rights reserved.

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_grasp/src/widget/dragging_auto_scroller.dart';
import 'package:flutter_grasp/src/widget/swappable.dart';

/// 拖动排序回调
typedef SwappableGroupCallback = void Function(Node fromNode, Node toNode);

/// 在未命中的时候，执行该回调
typedef SwappableGroupWillNotAcceptCallback = void Function(Node fromNode, Node toNode);

/// 自定拖动排序规则
typedef SwappableGroupHandler = Node Function(Node fromNode, Node toNode, bool dragging);

/// 构建[feedback]
typedef SwappableGroupPlaceholderBuilder = Widget Function(BuildContext context, Node node, Widget child);

int _sum(int a, int b) => a + b;

/// 拖动组的索引
final class Node {
  /// 拖动组的索引
  const Node(this.groupIndex, this.index) : assert(groupIndex >= 0 && index >= -1);

  /// 第0组的第0个
  static const zero = Node(0, 0);

  /// 所属组的索引
  final int groupIndex;

  /// 在组内的索引
  final int index;

  /// copy
  Node copyWith({
    int? groupIndex,
    int? index,
  }) {
    return Node(
      groupIndex ?? this.groupIndex,
      index ?? this.index,
    );
  }

  /// Returns a new node with index moved plus by the given delta.
  Node inflate(int delta) {
    return Node(groupIndex, index + delta);
  }

  /// Returns a new node with index moved minus by the given delta.
  Node deflate(int delta) {
    return inflate(-delta);
  }

  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Node && runtimeType == other.runtimeType && groupIndex == other.groupIndex && index == other.index;

  @override
  int get hashCode => groupIndex.hashCode ^ index.hashCode;

  @override
  String toString() {
    return 'Node{groupIndex: $groupIndex, index: $index}';
  }
}

/// Created by changlei on 2020/8/20.
///
/// 一组拖动排序
class SwappableGroup extends StatefulWidget {
  /// 一组拖动排序
  const SwappableGroup({
    super.key,
    required this.child,
    required this.itemCounts,
    this.onSwapHandler,
    this.onSwapEffect,
    this.onWillNotAccept,
    this.onDragSwap,
    this.onDragStart,
    this.onDragStarted,
    this.onDragUpdate,
    this.onDraggableCanceled,
    this.onDragCompleted,
    this.onDragEnd,
    this.onDragEnded,
  });

  /// child
  final Widget child;

  /// 触发排序
  final SwappableGroupCallback? onDragSwap;

  /// 自定义排序
  final SwappableGroupHandler? onSwapHandler;

  /// 排序效果
  final SwappableEffect? onSwapEffect;

  /// 在未命中的时候，执行该回调，注意，此方法只在拖动的时候执行，手动调用[SwappableState.swap]的时候，不执行
  final SwappableGroupWillNotAcceptCallback? onWillNotAccept;

  /// 拖动即将开始，即按下的时候
  final ValueChanged<Node>? onDragStart;

  /// Called when the draggable starts being dragged.
  final ValueChanged<Node>? onDragStarted;

  /// Called when the draggable is dragged.
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true), and if this widget has actually moved.
  final ValueChanged<Node>? onDragUpdate;

  /// Called when the draggable is dropped without being accepted by a [DragTarget].
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up being canceled, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
  final ValueChanged<Node>? onDraggableCanceled;

  /// Called when the draggable is dropped and accepted by a [DragTarget].
  ///
  /// This function might be called after this widget has been removed from the
  /// tree. For example, if a drag was in progress when this widget was removed
  /// from the tree and the drag ended up completing, this callback will
  /// still be called. For this reason, implementations of this callback might
  /// need to check [State.mounted] to check whether the state receiving the
  /// callback is still in the tree.
  final ValueChanged<Node>? onDragCompleted;

  /// Called when the draggable is dropped.
  ///
  /// The velocity and offset at which the pointer was moving when it was
  /// dropped is available in the [DraggableDetails]. Also included in the
  /// `details` is whether the draggable's [DragTarget] accepted it.
  ///
  /// This function will only be called while this widget is still mounted to
  /// the tree (i.e. [State.mounted] is true).
  final ValueChanged<Node>? onDragEnd;

  /// 拖动结束切动画一并结束了
  final ValueChanged<Node>? onDragEnded;

  /// 每个item对应的数量
  final List<int> itemCounts;

  @override
  State<SwappableGroup> createState() => SwappableGroupState();
}

/// 一组拖动排序
final class SwappableGroupState extends State<SwappableGroup> {
  final GlobalKey<SwappableState> _swapKey = GlobalKey<SwappableState>();

  int _itemCount = 0;
  Data? _lastData;

  SwappableState get _swap => _swapKey.currentState!;

  @override
  void initState() {
    _itemCount = widget.itemCounts.fold(0, _sum);
    super.initState();
  }

  @override
  void didUpdateWidget(SwappableGroup oldWidget) {
    if (!listEquals(widget.itemCounts, oldWidget.itemCounts)) {
      _itemCount = widget.itemCounts.fold(0, _sum);
    }
    super.didUpdateWidget(oldWidget);
  }

  int _expandNode(Node node) {
    return widget.itemCounts.take(node.groupIndex).fold(0, _sum) + node.index;
  }

  Node _foldIndex(int index) {
    if (index < 0) {
      return Node.zero;
    }
    final itemCounts = widget.itemCounts;
    if (index >= _itemCount) {
      return Node(itemCounts.length - 1, index - _itemCount);
    }
    int groupIndex = 0;
    int delta = 0;
    for (int i = 0; i < itemCounts.length; i++) {
      final itemCount = itemCounts[i];
      if (index >= delta - 1 && index < delta + itemCount) {
        groupIndex = i;
        break;
      }
      delta += itemCount;
    }
    return Node(groupIndex, index - delta);
  }

  /// 手动排序
  void swap(
    Node fromNode,
    Node toNode, {
    bool dragging = true,
    bool feedback = true,
  }) {
    final newToNode = widget.onSwapHandler?.call(fromNode, toNode, dragging);
    if (newToNode == fromNode) {
      return;
    }
    toNode = newToNode ?? toNode;
    _swap.swap(_expandNode(fromNode), _expandNode(toNode), dragging: dragging, feedback: false);
    if (dragging) {
      _lastData = Data(_swap, _expandNode(fromNode));
    } else {
      _lastData = null;
    }
    if (feedback) {
      widget.onDragSwap?.call(fromNode, toNode);
    }
  }

  void _onDragSwap(int fromIndex, int toIndex) {
    final fromNode = _foldIndex(fromIndex);
    final toNode = _foldIndex(toIndex);
    widget.onDragSwap?.call(fromNode, toNode);
    if (fromNode.groupIndex < toNode.groupIndex) {
      toIndex -= 1;
    }
    _lastData = Data(_swap, toIndex);
  }

  bool _onWillAccept(DragTargetDetails<Data> details, int toGroupIndex) {
    final data = details.data;
    final lastData = _lastData ??= data;
    final fromNode = _foldIndex(lastData.index);
    final fromGroupIndex = fromNode.groupIndex;
    if (widget.itemCounts[toGroupIndex] > 0 || fromGroupIndex == toGroupIndex) {
      return false;
    }
    int toIndex = 0;
    if (fromGroupIndex > toGroupIndex) {
      toIndex = widget.itemCounts[toGroupIndex] - 1;
    } else if (fromGroupIndex < toGroupIndex) {
      toIndex = 0;
    }
    Node toNode = Node(toGroupIndex, toIndex);
    final newToNode = widget.onSwapHandler?.call(fromNode, toNode, true);
    if (newToNode == fromNode) {
      return false;
    }
    toNode = newToNode ?? toNode;
    _swap.swap(lastData.index, _expandNode(toNode));
    return true;
  }

  void _onDragStart(Data data) {
    widget.onDragStart?.call(_foldIndex(data.index));
  }

  void _onDragStarted(Data data) {
    widget.onDragStarted?.call(_foldIndex(data.index));
  }

  void _onDragUpdate(Data data) {
    widget.onDragUpdate?.call(_foldIndex(data.index));
  }

  void _onDragEnd(Data data) {
    _lastData = null;
    setState(() {});
    widget.onDragEnd?.call(_foldIndex(data.index));
  }

  void _onDragEnded(Data data) {
    widget.onDragEnded?.call(_foldIndex(data.index));
  }

  void _onDraggableCanceled(Data data) {
    widget.onDraggableCanceled?.call(_foldIndex(data.index));
  }

  void _onDragCompleted(Data data) {
    widget.onDragCompleted?.call(_foldIndex(data.index));
  }

  int _onSwapHandler(int fromIndex, int toIndex, bool dragging) {
    final fromNode = _foldIndex(fromIndex);
    Node toNode = _foldIndex(toIndex);
    final newToNode = widget.onSwapHandler?.call(fromNode, toNode, dragging);
    if (newToNode == fromNode) {
      return fromIndex;
    }
    if (newToNode != toNode) {
      toNode = newToNode ?? toNode;
      toIndex = _expandNode(toNode);
    }
    if (fromNode.groupIndex < toNode.groupIndex) {
      toIndex -= 1;
    }
    return toIndex;
  }

  void _onWillNotAccept(int fromIndex, int toIndex) {
    final fromNode = _foldIndex(fromIndex);
    Node toNode = _foldIndex(toIndex);
    final newToNode = widget.onSwapHandler?.call(fromNode, toNode, true);
    if (newToNode == fromNode) {
      return;
    }
    if (newToNode != toNode) {
      toNode = newToNode ?? toNode;
      toIndex = _expandNode(toNode);
    }
    if (fromNode.groupIndex < toNode.groupIndex) {
      toIndex -= 1;
    }
    if (fromIndex == toIndex && fromNode != toNode) {
      widget.onDragSwap?.call(fromNode, toNode);
    } else {
      widget.onWillNotAccept?.call(fromNode, toNode);
    }
  }

  Function get _swappableConstructor => Swappable.new;

  @override
  Widget build(BuildContext context) {
    return Function.apply(
      _swappableConstructor,
      List.empty(),
      <Symbol, dynamic>{
        #key: _swapKey,
        #itemCount: _itemCount,
        #onSwapHandler: _onSwapHandler,
        #onSwapEffect: widget.onSwapEffect,
        #onWillNotAccept: _onWillNotAccept,
        #onDragSwap: _onDragSwap,
        #onDragStart: _onDragStart,
        #onDragStarted: _onDragStarted,
        #onDragUpdate: _onDragUpdate,
        #onDraggableCanceled: _onDraggableCanceled,
        #onDragCompleted: _onDragCompleted,
        #onDragEnd: _onDragEnd,
        #onDragEnded: _onDragEnded,
        #child: widget.child,
      },
    ) as Widget;
  }
}

/// 一组拖动排序
class SliverSwappableGroup extends SwappableGroup {
  /// 一组拖动排序
  const SliverSwappableGroup({
    super.key,
    required super.child,
    required super.itemCounts,
    super.onSwapHandler,
    super.onSwapEffect,
    super.onWillNotAccept,
    super.onDragSwap,
    super.onDragStart,
    super.onDragStarted,
    super.onDragUpdate,
    super.onDraggableCanceled,
    super.onDragCompleted,
    super.onDragEnd,
    super.onDragEnded,
  });

  @override
  State<SwappableGroup> createState() => SliverSwappableGroupState();
}

/// 一组拖动排序
final class SliverSwappableGroupState extends SwappableGroupState {
  @override
  Function get _swappableConstructor => SliverSwappable.new;
}

/// [SwappableGroup] item
class SwappableGroupTarget extends StatelessWidget {
  /// [SwappableGroup] item
  const SwappableGroupTarget({
    super.key,
    required this.groupIndex,
    required this.child,
  });

  /// 对应的索引
  final int groupIndex;

  /// child
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final state = context.findAncestorStateOfType<SwappableGroupState>()!;
    return DragTarget<Data>(
      onWillAcceptWithDetails: (details) => state._onWillAccept(details, groupIndex),
      builder: (context, candidateData, rejectedData) => child,
    );
  }
}

/// [SwappableGroup] item
class SwappableGroupChild extends StatelessWidget {
  /// [SwappableGroup] item
  const SwappableGroupChild({
    super.key,
    required this.node,
    this.feedbackBuilder,
    this.childWhenDraggingBuilder,
    this.dragTargetBuilder,
    this.duration = const Duration(milliseconds: 300),
    this.curve = Curves.linearToEaseOut,
    this.axis,
    this.autoScrollBoundary,
    this.ignoringFeedbackPointer = true,
    this.ignoringFeedbackSemantics = true,
    this.allowedButtonsFilter,
    this.alignment = Alignment.center,
    this.delay = kLongPressTimeout,
    this.dragAnchorStrategy = childDragAnchorStrategy,
    this.feedbackOffset = Offset.zero,
    this.hapticFeedbackOnStart = true,
    this.maxSimultaneousDrags,
    this.tickerOffset = true,
    required this.child,
  });

  /// 对应的索引
  final Node node;

  /// 构建拖动的feedback
  final SwappableGroupPlaceholderBuilder? feedbackBuilder;

  /// 构建拖动的child
  final SwappableGroupPlaceholderBuilder? childWhenDraggingBuilder;

  /// 构建[DragTarget.builder]
  final TargetChildBuilder? dragTargetBuilder;

  /// The length of time this animation should last.
  final Duration duration;

  /// The curve to use in the forward direction.
  final Curve curve;

  /// The [Axis] to restrict this draggable's movement, if specified.
  ///
  /// When axis is set to [Axis.horizontal], this widget can only be dragged
  /// horizontally. Behavior is similar for [Axis.vertical].
  ///
  /// Defaults to allowing drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// When null, allows drag on both [Axis.horizontal] and [Axis.vertical].
  ///
  /// For the direction of gestures this widget competes with to start a drag
  /// event, see [Draggable.affinity].
  final Axis? axis;

  /// 自动滚动边界
  final AutoScrollBoundary? autoScrollBoundary;

  /// Whether the [feedback] widget is ignored during hit testing.
  ///
  /// Regardless of whether this widget is ignored during hit testing, it will
  /// still consume space during layout and be visible during painting.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackPointer;

  /// Whether the semantics of the [feedback] widget is ignored when building
  /// the semantics tree.
  ///
  /// This value should be set to false when the [feedback] widget is intended
  /// to be the same object as the [child].  Placing a [GlobalKey] on this
  /// widget will ensure semantic focus is kept on the element as it moves in
  /// and out of the feedback position.
  ///
  /// Defaults to true.
  final bool ignoringFeedbackSemantics;

  /// {@macro flutter.gestures.multidrag._allowedButtonsFilter}
  final AllowedButtonsFilter? allowedButtonsFilter;

  /// The alignment of the child within the parent when the parent is not yet
  /// the same size as the child.
  final AlignmentGeometry alignment;

  /// The duration that a user has to press down before a long press is registered.
  ///
  /// Defaults to [kLongPressTimeout].
  final Duration delay;

  /// A strategy that is used by this draggable to get the anchor offset when it
  /// is dragged.
  ///
  /// The anchor offset refers to the distance between the users' fingers and
  /// the [feedback] widget when this draggable is dragged.
  ///
  /// This property's value is a function that implements [DragAnchorStrategy].
  /// There are two built-in functions that can be used:
  ///
  ///  * [childDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the original child.
  ///
  ///  * [pointerDragAnchorStrategy], which displays the feedback anchored at the
  ///    position of the touch that started the drag.
  final DragAnchorStrategy dragAnchorStrategy;

  /// The feedbackOffset can be used to set the hit test target point for the
  /// purposes of finding a drag target. It is especially useful if the feedback
  /// is transformed compared to the child.
  final Offset feedbackOffset;

  /// Whether haptic feedback should be triggered on drag start.
  final bool hapticFeedbackOnStart;

  /// How many simultaneous drags to support.
  ///
  /// When null, no limit is applied. Set this to 1 if you want to only allow
  /// the drag source to have one item dragged at a time. Set this to 0 if you
  /// want to prevent the draggable from actually being dragged.
  ///
  /// If you set this property to 1, consider supplying an "empty" widget for
  /// [childWhenDragging] to create the illusion of actually moving [child].
  final int? maxSimultaneousDrags;

  /// 是否响应offset动画
  final bool tickerOffset;

  /// child
  final Widget child;

  @override
  Widget build(BuildContext context) {
    final state = context.findAncestorStateOfType<SwappableGroupState>()!;
    SwappablePlaceholderBuilder? feedbackBuilder;
    if (this.feedbackBuilder case final builder?) {
      feedbackBuilder = (context, index, child) {
        return builder(context, state._foldIndex(index), child);
      };
    }
    SwappablePlaceholderBuilder? childWhenDraggingBuilder;
    if (this.childWhenDraggingBuilder case final builder?) {
      childWhenDraggingBuilder = (context, index, child) {
        return builder(context, state._foldIndex(index), child);
      };
    }
    return SwappableChild(
      index: state._expandNode(node),
      feedbackBuilder: feedbackBuilder,
      childWhenDraggingBuilder: childWhenDraggingBuilder,
      dragTargetBuilder: dragTargetBuilder,
      duration: duration,
      curve: curve,
      axis: axis,
      autoScrollBoundary: autoScrollBoundary,
      ignoringFeedbackPointer: ignoringFeedbackPointer,
      ignoringFeedbackSemantics: ignoringFeedbackSemantics,
      allowedButtonsFilter: allowedButtonsFilter,
      alignment: alignment,
      delay: delay,
      dragAnchorStrategy: dragAnchorStrategy,
      feedbackOffset: feedbackOffset,
      hapticFeedbackOnStart: hapticFeedbackOnStart,
      maxSimultaneousDrags: maxSimultaneousDrags,
      tickerOffset: tickerOffset,
      child: child,
    );
  }
}
